﻿using Levfier.Proccessors.Parameters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Levfier.Proccessors.Measuring
{
    /// <summary>
    /// 更新通知
    /// </summary>
    public delegate void Update();

    public class SpectrumAnalyzer : Proccessor
    {
        /// <summary>
        /// 更新頻度
        /// </summary>
        private static readonly int UpdateFrequency = 50;

        /// <summary>
        /// 更新を通知する
        /// </summary>
        public event Update UpdateNotify;

        /// <summary>
        /// パワースペクトル
        /// </summary>
        public double[] PowerSpectrum { get; set; }

        /// <summary>
        /// 直近のパワースペクトル最大値
        /// </summary>
        public double[] MaxPowerSpectrum { get; set; }

        /// <summary>
        /// 周波数精度[Hz]
        /// </summary>
        public double FreqAccuracy { get; set; }

        /// <summary>
        /// Binの精度[Hz]
        /// </summary>
        private ProccessParameter accuracyParam;

        /// <summary>
        /// ゲインに関するパラメータ
        /// </summary>
        private ProccessParameter gainParam;

        /// <summary>
        /// マックスホールドの減衰率に関するパラメータ
        /// </summary>
        private ProccessParameter decayRateParam;

        /// <summary>
        /// サンプルカウント
        /// </summary>
        private int sampleCount;

        /// <summary>
        /// FFTフレームのサイズ
        /// 2の冪乗
        /// </summary>
        private int frameSize;

        /// <summary>
        /// フレームサイズの2の対数
        /// </summary>
        private int bitSize;

        /// <summary>
        /// 入力の生データ
        /// </summary>
        private double[] rawData;

        /// <summary>
        /// 入力信号
        /// </summary>
        private double[] inputReal;

        /// <summary>
        /// 入力信号の複素数領域
        /// ダミーとして使う
        /// </summary>
        private double[] inputImage;

        /// <summary>
        /// FFT結果
        /// </summary>
        public double[] outputReal;

        /// <summary>
        /// FFT結果の複素数領域
        /// </summary>
        private double[] outputImage;

        /// <summary>
        /// 窓関数の係数
        /// フレームサイズ分でなく、サンプルカウント分を確保
        /// </summary>
        private double[] window;

        /// <summary>
        /// 更新カウンタ
        /// </summary>
        private int updateCount;

        /// <summary>
        /// 更新カウンタ
        /// </summary>
        private double[] frame;

        /// <summary>
        /// ロック用のオブジェクト
        /// </summary>
        private object lockObj = new object();

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SpectrumAnalyzer()
            : base()
        {
            //サンプルカウントが変化した時のイベントハンドラを登録
            VSTParameterManager.GetInstance().SampleCountChanged += SpectrumAnalyzer_SampleCountChanged;
            VSTParameterManager.GetInstance().SampleRateChanged += SpectrumAnalyzer_SampleCountChanged;
        }

        /// <summary>
        /// 入力された音を処理する
        /// テンプレートメソッドパターン
        /// </summary>
        /// <param name="input">入力時系列データ</param>
        public override void Proccess(double[] input)
        {
            if (null != this.lowerChain)
            {
                if (!this.bypass)
                {
                    //処理する
                    this.InnerProccess(input, this.lowerChain.GetBuffer());                    
                }

                //バイパス処理
                base.ProccessBypass(input, this.lowerChain.GetBuffer());

                //後段のストリームへ流す
                lowerChain.FlowSounds();
            }
            else
            {
                //処理する
                this.InnerProccess(input, null);
            }
        }

        /// <summary>
        /// サンプルカウント サンプル周波数が変更された時の動作
        /// </summary>
        /// <param name="value"></param>
        private void SpectrumAnalyzer_SampleCountChanged(double value)
        {
            this.InitFFT();
        }

        /// <summary>
        /// 音を処理する
        /// </summary>
        /// <param name="inputBuffer">入力</param>
        /// <param name="outputBuffer">出力</param>
        protected override void InnerProccess(double[] inputBuffer, double[] outputBuffer)
        {
            double gain = ProccessParameter.ConvertDBToValue(this.gainParam.Value);
            double decayRate = this.decayRateParam.Value;

            lock (lockObj)
            {
                this.PushBuffer(inputBuffer);

                //更新周期でのみFFTする
                if (UpdateFrequency < updateCount)
                {
                    //生データに窓関数を適用し、格納
                    this.ApplyWindow(this.frame, this.inputReal);

                    //フーリエ変換
                    CommonMath.FFT(this.inputReal, this.inputImage, out this.outputReal, out this.outputImage, this.bitSize);

                    for (int i = 0; i < this.frameSize; i++)
                    {
                        this.PowerSpectrum[i] = gain * Math.Sqrt(this.outputReal[i] * this.outputReal[i] + this.outputImage[i] * this.outputImage[i]);

                        this.MaxPowerSpectrum[i] *= decayRate;

                        if (this.MaxPowerSpectrum[i] < this.PowerSpectrum[i])
                        {
                            this.MaxPowerSpectrum[i] = this.PowerSpectrum[i];
                        }
                    }

                    //更新通知
                    if (null != this.UpdateNotify)
                    {
                        this.UpdateNotify();
                    }
                    updateCount = 0;
                }
                updateCount++;
            }
        }

        /// <summary>
        /// パラメータの初期化
        /// </summary>
        protected override void InitParameters()
        {
            this.accuracyParam = new ProccessParameter();
            this.accuracyParam.Name = "Accuracy[Hz]";
            this.accuracyParam.Value = 500.0d;
            this.accuracyParam.MaxValue = 1000.0d;
            this.accuracyParam.MinValue = 50.0d;
            this.accuracyParam.Step = 50.0d;
            this.accuracyParam.ValueChanged += AccuracyParam_ValueChanged;
            this.parameters.Add(this.accuracyParam.Name, this.accuracyParam);

            this.gainParam = new ProccessParameter();
            this.gainParam.Name = "Magnification";
            this.gainParam.Value = 0.0d;
            this.gainParam.MaxValue = 100.0d;
            this.gainParam.MinValue = -100.0d;
            this.gainParam.Step = 1.0d;
            this.parameters.Add(this.gainParam.Name, this.gainParam);

            this.decayRateParam = new ProccessParameter();
            this.decayRateParam.Name = "Decay Rate";
            this.decayRateParam.Value = 0.8d;
            this.decayRateParam.MaxValue = 1.0d;
            this.decayRateParam.MinValue = 0.8d;
            this.decayRateParam.Step = 0.01d;
            this.parameters.Add(this.decayRateParam.Name, this.decayRateParam);

            this.InitFFT();
        }

        /// <summary>
        /// ビン精度周波数が変更された時の動作
        /// </summary>
        /// <param name="sender"></param>
        private void AccuracyParam_ValueChanged(object sender)
        {
            this.InitFFT();
        }

        /// <summary>
        /// FFTの初期化
        /// </summary>
        private void InitFFT()
        {
            lock (lockObj)
            {
                //周波数精度
                double accuracyFreq = this.accuracyParam.Value;

                double sampleRate = VSTParameterManager.GetInstance().SampleRate;
                this.sampleCount = VSTParameterManager.GetInstance().SampleCount;
                this.frameSize = 1;

                //2の冪乗の最低フレームサイズを求める
                for (int i = 0; this.frameSize < sampleCount; i++)
                {
                    this.frameSize = (int)Math.Pow(2, i);
                    this.bitSize = i;
                }

                //設定された周波数精度を満たすフレームサイズを求める
                for (int i = this.bitSize; (sampleRate / (double)this.frameSize) > accuracyFreq; i++)
                {
                    this.frameSize = (int)Math.Pow(2, i);
                    this.bitSize = i;
                }

                //実際の周波数精度
                this.FreqAccuracy = sampleRate / (double)this.frameSize;

                this.rawData = new double[this.sampleCount];

                this.inputReal = new double[this.frameSize];
                this.inputImage = new double[this.frameSize];

                this.PowerSpectrum = new double[this.frameSize];
                this.MaxPowerSpectrum = new double[this.frameSize];

                this.frame = new double[this.frameSize];

                //窓関数は実データ部分のみ
                this.window = new double[this.frameSize];

                //窓係数を初期化
                this.InitWindow();
            }
        }

        /// <summary>
        /// 窓係数を初期化する
        /// </summary>
        private void InitWindow()
        {
            //窓関数を初期化する
            for (int i = 0; i < this.frameSize; i++)
            {
                this.window[i] = CommonMath.GetHanningWindowValue(i, this.sampleCount);
            }
        }

        /// <summary>
        /// 窓関数を適用する
        /// </summary>
        private void ApplyWindow(double[] input, double[] output)
        {
            //実データのみに窓関数を適用する
            for (int i = 0; i < input.Length; i++)
            {
                output[i] = this.window[i] * input[i];
            }
        }

        /// <summary>
        /// 入力音をフレームの奥にずらす
        /// </summary>
        /// <param name="source"></param>
        private void PushBuffer(double[] source)
        {
            int length = this.frameSize - this.sampleCount;

            //サンプルカウント分押し込む
            for (int i = length - 1; i >= 0; i--)
            {
                this.frame[this.sampleCount + i] = this.frame[i];
            }

            //入力を頭に挿入
            for(int i = 0 ; i < this.sampleCount ; i++)
            {
                this.frame[i] = source[i];
            }
        }
    }
}
